package 
{
	// Flash Molehill : Massive amounts of  3d particles with GPU 
	// Jean-Philippe Auclair (jpauclair)
	// jpauclair.net
	
	// PBender -> http://www.unitzeroone.com/blog/2009/03/18/flash-10-massive-amounts-of-3d-particles-with-alchemy-source-included/
	// AS3     -> http://blog.joa-ebert.com/2009/04/03/massive-amounts-of-3d-particles-without-alchemy-and-pixelbender/
	// haXe    -> http://webr3.org/blog/haxe/flash-10-massive-amounts-of-3d-particles-with-haxe/
	
	import com.adobe.utils.AGALMiniAssembler;
	import com.buraks.utils.fastmem;
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.display.Sprite;
	import flash.display.Stage3D;
	import flash.display.StageScaleMode;
	import flash.display3D.Context3D;
	import flash.display3D.Context3DBlendFactor;
	import flash.display3D.Context3DCompareMode;
	import flash.display3D.Context3DProgramType;
	import flash.display3D.Context3DRenderMode;
	import flash.display3D.Context3DTextureFormat;
	import flash.display3D.Context3DTriangleFace;
	import flash.display3D.Context3DVertexBufferFormat;
	import flash.display3D.IndexBuffer3D;
	import flash.display3D.Program3D;
	import flash.display3D.textures.Texture;
	import flash.display3D.VertexBuffer3D;
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.geom.Matrix3D;
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.geom.Vector3D;
	import flash.system.Capabilities;
	import flash.utils.ByteArray;
	import flash.utils.Endian;
	import flash.utils.Timer;

	[SWF(width = "550", height = "550", frameRate = "60", backgroundColor = "#0")]
	public class Main extends Sprite 
	{
		public var _renderTimer:Timer;
		
		private const MAX_PARTICLES:uint = 1 * ((Math.pow(2, 16)) / 4 - 1); //Limited to 16 bits (2^16) (64535) (with 4 vertice per quad) = 16K particles
		private const MAX_PARTICLES_PER_DRAW: uint = (Math.pow(2,16))/4-1; //Limited to 16 bits (2^16) (64535) (with 4 vertice per quad) = 16K particles
		
		private var _context:Context3D;
		private var _projectionMatrix:Matrix3D;
		private var _viewMatrix:Matrix3D;
		private var _invertViewMatrix:Matrix3D = new Matrix3D();
		
		private var _numCol:int = 16;
		private var _numRow:int = 5;
		
		private var _currentParticleFrame:Vector.<int> = new Vector.<int>(MAX_PARTICLES, true);
		
		private var _singleFrameUVWidth:Number = 32/512;
		private var _singleFrameUVHeight:Number = 32/512;			
		
		private var _animFrameCount:Number = 71;// 8 * 8;			
		
		private var _vertexBufferData:ByteArray = null;
		private var _vertexBuffer:VertexBuffer3D= null;
		
		private var _indexBufferData:ByteArray = null;
		private var _indexBuffer:IndexBuffer3D = null;
		private var _texture:Texture = null;
		private var _textureMask:Texture = null;
		private var _fragmentConsts:Vector.<Number> = Vector.<Number>([ 0, 0, 1, 0.05]);	
		
		// Boid specifics
		private const HEIGHT:int = 10;
		private const WIDTH:int = 10;
		
		private var mBoidList:Vector.<Boid> = null;
		private var mScreenCenterX:Number = 0;
		private var mScreenCenterY:Number = 0;
		
		[Embed(source="../lib/scblack.png")]
		private var _SpriteSheetATF:Class;		
		
		[Embed(source="../lib/scmask.png")]
		private var _SpriteSheetMaskATF:Class;		
		
		private var _UseIncubator:Boolean = false;
		
		public function Main():void 
		{
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			_renderTimer = new Timer(1);
			this.addEventListener(Event.ENTER_FRAME, OnEnterFrame);
			
			SetupContext();
		}
		
		private function SetupContext() : void
		{
			trace(Capabilities.version)
			
			var a:Object;
			
			try {
				this.stage.stage3Ds[0]["viewPort"] = new Rectangle(0, 0, stage.stageWidth, stage.stageHeight);
				_UseIncubator = false;
			}
			catch (e:Error)
			{
				_UseIncubator = true;
			}
			
			this.stage.stage3Ds[0].addEventListener ( "context3DCreate", OnContext );
			this.stage.stage3Ds[0].requestContext3D(Context3DRenderMode.AUTO)	
		}
		
		private function OnContext(e:Event):void 
		{
			var t : Stage3D = e.target as Stage3D;
			_context = t.context3D; 	
			_context.enableErrorChecking = true;
			
			if (_UseIncubator )
			{
				this.stage.stage3Ds[0]["x"] = 0
				this.stage.stage3Ds[0]["y"] = 0
			}
			
			this.stage.scaleMode = StageScaleMode.NO_SCALE;
			mScreenCenterX = 0;
			mScreenCenterY = 0;
			_context.configureBackBuffer(this.stage.stageWidth, this.stage.stageHeight,8,true); // fixed size
			_context.setCulling(Context3DTriangleFace.BACK);
			_context.setDepthTest(true, Context3DCompareMode.LESS); 
			_context.setBlendFactors( Context3DBlendFactor.SOURCE_ALPHA, Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA );				
			
			Rndm.seed = int.MAX_VALUE * Math.random();
			CreateBoids();
			CreateParticles();
			
			InitStreams();
			
			InitProgram();
			
			InitProgramConstants();
			
			InitTextures(); 
			
			InitCamera();
		}
		
		private function CreateBoids():void
		{
			mBoidList = new Vector.<Boid>(MAX_PARTICLES, true);
			var len:int = mBoidList.length;
			for (var i:int = 0; i < len; i++)
			{
				mBoidList[i] = new Boid();
			}
			
			// Assign
			for (i = 0; i < len; i++)
			{
				Assign(mBoidList[i], i);
				mBoidList[i].SetBindPos(mScreenCenterX, mScreenCenterY, mScreenCenterY);
				RandomPos(mBoidList[i]);
			}
		}
		
		private function Assign(aBoid:Boid, aIndex:int):void
		{
			var randomFriend1:int = 0;
			do
			{
				randomFriend1 = RandomInt(0, MAX_PARTICLES - 1);
			}
			while (aIndex == randomFriend1)
			
			var randomFriend2:int = 0;
			do
			{
				randomFriend2 = RandomInt(0, MAX_PARTICLES - 1);
			}
			while (aIndex == randomFriend2 || randomFriend1 == randomFriend2)
			
			aBoid.mFriend = mBoidList[randomFriend1];
			aBoid.mEnemy = mBoidList[randomFriend2];
		}
		
		private var mFrameCpt:int = 10;
		private function UpdateLogic():void
		{
			var len:int = mBoidList.length;
			for (var i:int = 0; i < len; i++)
			{
				mBoidList[i].Update();
			}
			
			mFrameCpt--;
			
			if (mFrameCpt <= 0)
			{
				mFrameCpt = 10;
				
				var rnd:int = RandomInt(0, mBoidList.length - 1);
				Assign(mBoidList[rnd], rnd);
				RandomPos(mBoidList[rnd]);
			}
		}
		
		private function RandomInt(aMin:int, aMax:int):int
		{
			return (Math.floor(Rndm.random() * (aMax - aMin + 1)) + aMin); 
		}
		
		private function RandomNumber(aMin:Number, aMax:Number):Number
		{
			return aMin + (Rndm.random() * (aMax - aMin));
		}
		
		private function RandomPos(aBoid:Boid):void
		{
			aBoid.mPos.x = RandomNumber(-stage.stageWidth / 2, stage.stageWidth);
			aBoid.mPos.y = RandomNumber(-stage.stageHeight / 2, stage.stageHeight / 2);
			aBoid.mPos.z = RandomNumber(-stage.stageHeight / 2, stage.stageHeight / 2);
		}
		
		public function CreateParticles() : void
		{
			var positions:Vector.<Vector3D> = new Vector.<Vector3D>(MAX_PARTICLES * 4,true);
			var indexes:Vector.<uint> = new Vector.<uint>(MAX_PARTICLES * 6,true);
			var uvs:Vector.<Point> = new Vector.<Point>(MAX_PARTICLES * 4, true);
			var quad_corners:Vector.<Point> = new Vector.<Point>(MAX_PARTICLES * 4, true);
			
			var vertexPositionIdx:int = 0;
			var vertexIndexIdx:int = 0;
			var CornerIdx:int = 0;
			var vertexUVIdx:int = 0;
			
			var it:int = 0;
			var quadSizeX:Number = 0.1;
			var quadSizeY:Number = 0.1;
			var index:int = 0;
			
			while(it < MAX_PARTICLES)
			{
				var x:Number = 0;
				var y:Number = 0;
				var z:Number = 0;
				
				positions[vertexPositionIdx++] = new Vector3D( x, y, z, 12);
				positions[vertexPositionIdx++] = new Vector3D( x, y, z, 13);
				positions[vertexPositionIdx++] = new Vector3D( x, y, z, 14);
				positions[vertexPositionIdx++] = new Vector3D( x, y, z, 15);
				
				indexes[vertexIndexIdx++] = 0 + it * 4;
				indexes[vertexIndexIdx++] = 1 + it * 4;
				indexes[vertexIndexIdx++] = 2 + it * 4;
				
				indexes[vertexIndexIdx++] = 2 + it * 4;
				indexes[vertexIndexIdx++] = 1 + it * 4;
				indexes[vertexIndexIdx++] = 3 + it * 4;
				
				uvs[vertexUVIdx++] = new Point(0, 0);
				uvs[vertexUVIdx++] = new Point(_singleFrameUVWidth, 0);
				uvs[vertexUVIdx++] = new Point(0, _singleFrameUVHeight);
				uvs[vertexUVIdx++] = new Point(_singleFrameUVWidth, _singleFrameUVHeight);
				
				quad_corners[CornerIdx++] = new Point( -quadSizeX, -quadSizeY);
				quad_corners[CornerIdx++] = new Point( quadSizeX, -quadSizeY);
				quad_corners[CornerIdx++] = new Point( -quadSizeX, quadSizeY);
				quad_corners[CornerIdx++] = new Point( quadSizeX, quadSizeY);				
				
				_currentParticleFrame[it] = Math.random() * 71;
				
				it++;
			}
			
			SetFastMem(positions, indexes, uvs, quad_corners);	
			
			fastmem.fastSelectMem(_vertexBufferData);
		}
		
		public function InitStreams() : void
		{
			_context.setVertexBufferAt( 0, _vertexBuffer, 0, Context3DVertexBufferFormat.FLOAT_4  ); // XYZW
			_context.setVertexBufferAt( 1, _vertexBuffer, 4, Context3DVertexBufferFormat.FLOAT_2  ); // UV			
			_context.setVertexBufferAt( 2, _vertexBuffer, 6, Context3DVertexBufferFormat.FLOAT_2  ); // UV			
		}
		
		public function InitProgramConstants() : void
		{
			var quadSizeX:Number = 0.075*0.2;
			var quadSizeY:Number = 0.075*0.2;
			
			_context.setProgramConstantsFromVector( Context3DProgramType.FRAGMENT, 0,_fragmentConsts, 1 );
			
			var vertexOffset:Vector.<Number> = null;
			
			vertexOffset = Vector.<Number>([-quadSizeX, -quadSizeY, 0, 1]);
			_context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 12,vertexOffset, 1);	
			
			vertexOffset = Vector.<Number>([quadSizeX, -quadSizeY, 0, 1]);
			_context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 13,vertexOffset, 1);	
			
			vertexOffset = Vector.<Number>([ -quadSizeX, quadSizeY, 0, 1]);
			_context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 14,vertexOffset, 1);	
			
			vertexOffset = Vector.<Number>([quadSizeX, quadSizeY, 0, 1]);
			_context.setProgramConstantsFromVector(Context3DProgramType.VERTEX, 15,vertexOffset, 1);					
			
		}
		
		public function InitTextures() : void
		{
			var imgb:Bitmap = new _SpriteSheetATF() as Bitmap;
			var img:BitmapData = imgb.bitmapData
			_texture = _context.createTexture(512, 512, Context3DTextureFormat.BGRA, false );
			
			_texture.uploadFromBitmapData(img,0);
			_context.setTextureAt(0, _texture);			
			
			var img2b:Bitmap = new _SpriteSheetMaskATF() as Bitmap;
			var img2:BitmapData = img2b.bitmapData
			_texture = _context.createTexture(512, 512, Context3DTextureFormat.BGRA, false );
			
			_texture.uploadFromBitmapData(img2,0);		
		}
		
		public function InitCamera():void
		{
			_viewMatrix = new Matrix3D();
			_projectionMatrix = makeProjectionMatrix();
		}
		
		public function SetFastMem(positions:Vector.<Vector3D>, indexes:Vector.<uint>, uvs:Vector.<Point>, corners:Vector.<Point>) : void
		{
			var tempAllocation:ByteArray = new ByteArray();
			tempAllocation.length = 1024 * 1024 * 3;
			tempAllocation.endian = Endian.LITTLE_ENDIAN;	
			fastmem.fastSelectMem(tempAllocation);
			
			var len:int = indexes.length
			var i:int = 0;

			for (i = 0; i < len; i++)
			{
				fastmem.fastSetI16(indexes[i], i * 2);
			}
			
			tempAllocation.position = 0;
			
			_indexBufferData = new ByteArray();
			_indexBufferData.endian = Endian.LITTLE_ENDIAN;
			_indexBufferData.length = len * 2;
			_indexBufferData.writeBytes(tempAllocation,0, _indexBufferData.length);			
			
			_indexBuffer = _context.createIndexBuffer(len);
			_indexBuffer.uploadFromByteArray(_indexBufferData, 0, 0, len);
			
			var dwordPerVertice:int = 8;
			
			var vertexCount:int = positions.length;
			
			var pos:Vector3D = null;
			var uv:Point = null;
			
			var corner:Point = null;
			
			var outPos:int = 0;
			
			for (i = 0; i < vertexCount; i++)
			{
				// X Y Z
				pos = positions[i];
				fastmem.fastSetFloat(pos.x,  outPos); outPos += 4; // X
				fastmem.fastSetFloat(pos.y , outPos); outPos += 4; // Y
				fastmem.fastSetFloat(pos.z , outPos); outPos += 4; // Z
				fastmem.fastSetFloat(pos.w , outPos); outPos += 4; // w

				// U V
				uv = uvs[i];
				fastmem.fastSetFloat(uv.x , outPos); outPos += 4; // U
				fastmem.fastSetFloat(uv.y , outPos); outPos += 4; // V
				
				// U V
				corner = corners[i];
				fastmem.fastSetFloat(corner.x , outPos); outPos += 4; // U
				fastmem.fastSetFloat(corner.y , outPos); outPos += 4; // V					
			}
			
			tempAllocation.position = 0;
			
			_vertexBufferData = new ByteArray();
			_vertexBufferData.endian = Endian.LITTLE_ENDIAN;
			_vertexBufferData.length = vertexCount * (4 * dwordPerVertice);
			_vertexBufferData.writeBytes(tempAllocation, 0, _vertexBufferData.length);
			
			_vertexBuffer = _context.createVertexBuffer(vertexCount, dwordPerVertice);
			_vertexBuffer.uploadFromByteArray(_vertexBufferData, 0, 0, vertexCount);

		}

		private function HandleTimer (e:TimerEvent) : void
		{
			OnEnterFrame ( null);		
		}
		
		private var viewRotationSpeed:Number = 33.1;
		private var viewCurrentRotation1:Number = 0.1;
		
		private var mNumberToShow:int = 1;
		private var _sizeToUse:int = 1;
		private function OnEnterFrame(e:Event):void 
		{
			UpdateLogic();
			_context.clear(0, 0, 0, 0);
			
			var spacer:Number = 0;
			var distZ:Number = 20;
			
			//viewCurrentRotation1 += viewRotationSpeed;
			UpdatePosition();
			_viewMatrix.identity();
			//_viewMatrix.appendRotation(viewCurrentRotation1/ 53,	 Vector3D.Z_AXIS);
			//_viewMatrix.appendRotation(viewCurrentRotation1/ 29,	 Vector3D.Y_AXIS);
			//_viewMatrix.appendRotation(viewCurrentRotation1 / 71, Vector3D.X_AXIS); // the classic prime spin
			_viewMatrix.copyToMatrix3D(_invertViewMatrix);
			_viewMatrix.appendTranslation(-0, -0, -23); // the classic prime spin
			_viewMatrix.append(_projectionMatrix);
			_invertViewMatrix.invert();
			
			_context.setProgramConstantsFromMatrix( Context3DProgramType.VERTEX, 0, _viewMatrix,true );
			_context.setProgramConstantsFromMatrix( Context3DProgramType.VERTEX, 8, _invertViewMatrix,true );
			
			_context.drawTriangles( _indexBuffer, 0, MAX_PARTICLES * 2);
			
			_context.present();
		}
		
		private function UpdatePosition() : void
		{
			var offsetVertexBuffer:int = 0;
			var vertexDescriptionSize:int = 8*4;
			var boid:Boid = null;
			for (var i:int = 0; i < MAX_PARTICLES; i++)
			{
				boid = mBoidList[i];
				var newPosX:Number = boid.mPos.x;
				var newPosY:Number = boid.mPos.y;
				var newPosZ:Number = boid.mPos.z;
				var localOffsetPos:int = localOffsetPos = offsetVertexBuffer + 0;
				
				//UV vertex 1
				fastmem.fastSetFloat( newPosX, localOffsetPos);
				fastmem.fastSetFloat( newPosY, localOffsetPos + 4);
				fastmem.fastSetFloat( newPosZ, localOffsetPos + 8);
				localOffsetPos += vertexDescriptionSize;
				
				//UV vertex 2
				fastmem.fastSetFloat( newPosX, localOffsetPos);
				fastmem.fastSetFloat( newPosY, localOffsetPos + 4);
				fastmem.fastSetFloat( newPosZ, localOffsetPos + 8);
				localOffsetPos += vertexDescriptionSize;
				
				//UV vertex 3
				fastmem.fastSetFloat( newPosX, localOffsetPos);
				fastmem.fastSetFloat( newPosY, localOffsetPos + 4);
				fastmem.fastSetFloat( newPosZ, localOffsetPos + 8);
				localOffsetPos += vertexDescriptionSize;
				
				//UV vertex 4
				fastmem.fastSetFloat( newPosX, localOffsetPos);
				fastmem.fastSetFloat( newPosY, localOffsetPos + 4);
				fastmem.fastSetFloat( newPosZ, localOffsetPos + 8);
				localOffsetPos += vertexDescriptionSize;
				
				//Jump one quad ahead
				offsetVertexBuffer += vertexDescriptionSize * 4;
			}
			
			_vertexBuffer.uploadFromByteArray(_vertexBufferData, 0, 0, _vertexBufferData.length / vertexDescriptionSize);
		}
		
		private function InitProgram() : void
		{
			var vertexASM:AGALMiniAssembler = new AGALMiniAssembler();

			vertexASM.assemble( Context3DProgramType.VERTEX, 
					"mov vt1, va0 \n" + 
					"sub vt2.z , vt1.x, vt1.x \n" +  			// new vector (DeltaX,DeltaY,DeltaZ,1)
					"mov vt2.xy, va2.xy \n"+
					"m33 vt3.xyz, vt2.xyz, vc8     \n" +  		// transform InvertView (m33 because it's only a rotation/scale)
					"mov vt3.w, vc11.w \n" + 
					"add vt3.xyz, vt3.xyz, vt1.xyz \n" +// Add the original vertex position
					
					"m44 op, vt3, vc0     \n" +  		// transform ViewProjection matrix
					"mov v1, va1      \n");				// Send UVs to fragment shader
			
			var fragmentASM:AGALMiniAssembler = new AGALMiniAssembler();
			fragmentASM = new AGALMiniAssembler(); 
			fragmentASM.assemble( Context3DProgramType.FRAGMENT,
				"tex ft1, v1, fs0  <2d,linear,nomip> \n" +  //Diffuse
				//"tex ft2, v1, fs1  <2d,linear,nomip> \n" +  //AlphaMask
				
				"sub ft0.x ,ft1.x,fc0.w \n" + //Substract transparency treshold
				"kil ft0.x \n" + //Kill alpha pixels
				
				"mov oc, ft1"  );	
			
			var pr:Program3D = _context.createProgram();
			
			pr.upload(vertexASM.agalcode, fragmentASM.agalcode);	
			
			_context.setProgram(pr);
		}
		
		public static const DEG2RAD:Number	= Math.PI / 360.0;
		
		public function makeProjectionMatrix ( zNear:Number = 0.1, 
													  zFar:Number = 1000, 
													  fovDegrees:Number = 60, 
													  aspect:Number = 4/3 ):Matrix3D
		{  			
			var yval:Number = zNear * Math.tan( fovDegrees * DEG2RAD );
			var xval:Number = yval * aspect;	
			return makeFrustumMatrix( -xval, xval, -yval, yval, zNear, zFar );
		}				
		
		public function makeFrustumMatrix( left:Number, right:Number, top:Number, bottom:Number, zNear:Number, zFar:Number ):Matrix3D 
		{
			return new Matrix3D(
				Vector.<Number>(
					[
						(2*zNear)/(right-left),
						0,
						(right+left)/(right-left),
						0,
						
						0,
						(2*zNear)/(top-bottom),
						(top+bottom)/(top-bottom),
						0,
						
						0,
						0,
						zFar/(zNear-zFar),
						-1,
						
						0,
						0,
						(zNear*zFar)/(zNear-zFar),
						0
					]
				)
			);
		}
	}
}